import java.awt.Point;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.ReentrantLock;

public class Simulation
{
    private class SimulationTickData
    {
        private int TickCount;

        public SimulationTickData()
        {
            TickCount = 0;
        }
    }

    private SimulationTickData SimulationTickData;

    private class SimulationTickThread extends Thread
    {
        @Override
        public void run()
        {
            // long StartTime = System.currentTimeMillis();
            SimulationTick();
            // System.out.println(System.currentTimeMillis() - StartTime);
        }
    }

    private final int SimulationIOProgramLineNumberMax = 1000000;

    private int SlowDownDroppedTicksDesired = 0;
    private int SlowDownDroppedTicksCurrent = 0;

    private final static int TicksPerSecond = 30; // don't use too few or too much delay when passing Line chains, do not use too much or too high CPU load
    private final static int TicksPerIOTextAreaLine = 6; // the more, the slower the ILines are worked up

    public static int GetTicksPerSecond()
    {
        return TicksPerSecond;
    }

    public static int TicksPerIOTextAreaLine()
    {
        return TicksPerIOTextAreaLine;
    }

    private MainWindow.GUIElements GUIElements;
    private IOProgram IOProgram;
    private ObjectStorage ObjectStorage;
    private Ticker Ticker;

    private Timer TickTimer = null; // while (true)-loop did not work (locks GUI)
    private boolean TickTimerRunning = false; // if running, set to false to end simulation
    private boolean TickTimerPaused = false;
    private TimerTask TickTimerTask;
    private ReentrantLock TickTimerMutex = new ReentrantLock(); // see code...
    private int TickTimerEventsLostCount = 0;

    public Simulation(MainWindow.GUIElements GUIElements, IOProgram IOProgram, Ticker Ticker, ObjectStorage ObjectStorage)
    {
        this.GUIElements = GUIElements;
        this.IOProgram = IOProgram;
        this.ObjectStorage = ObjectStorage;
        this.Ticker = Ticker;
    }

    public boolean IsSimulationPaused()
    {
        return TickTimerPaused;
    }

    public boolean IsSimulationRunning()
    {
        return TickTimerRunning;
    }

    public void PauseSimulation()
    {
        TickTimerMutex.lock();
        if (TickTimerRunning)
        {
            TickTimerPaused = true;
        }
        TickTimerMutex.unlock();
    }

    private void ProcessActivatedObjects()
    { // apply additional actions to (selected) activated DrawObjects after each simulation tick

        String TextFieldTextLast = "";

        ArrayList<DrawObject> ActivatedTextFields = ObjectStorage.GetActivatedObjects(new TextField(new Point(0, 0)));
        for (Iterator<DrawObject> i = ActivatedTextFields.iterator(); i.hasNext();)
        {
            DrawObject o = i.next();
            if (o.IsTextField()) // additional check (actually surplus, but just to make sure), ObjectStorage.GetActivatedObjects([...]) should have returned TextFields only
            {
                IOProgram.ReactOnTextFieldActivation((TextField) o);

                String TextFieldText = ((TextField) o).GetText();

                if (TextFieldText.indexOf("=") >= 0)
                {
                    ObjectStorage.UpdateHormoneStrengthForAll(Neuron.ExtractHormoneName(TextFieldText), Neuron.ExtractHormoneStrength(TextFieldText));
                }

                if (!(TextFieldText.equals(TextFieldTextLast)))
                {
                    // NOTE (2022-08-13_14-33): Ticker is currently disabled as turned out
                    // to be rather useless for understanding (rather confusing), enable
                    // just the following code line to enable it again, if desired:

                    // Ticker.ReceiveString(TextFieldText + "... ");
                }
                TextFieldTextLast = TextFieldText;
            }
        }
    }

    public void ResumeSimulation()
    {
        TickTimerMutex.lock();
        if (TickTimerRunning)
        {
            TickTimerPaused = false;
        }
        TickTimerMutex.unlock();
    }

    private void SimulationTick()
    {
        // verify
        if (TickTimerRunning == false) // maybe StopSimulation() called
        {
            // DO NOT lock TickTimerMutex here or deadlock (UserActionManager locked it via PauseSimulation() and then TickTimer event calls this here...)
            return;
        }
        if (TickTimerPaused)
        {
            // DO NOT lock TickTimerMutex here or deadlock
            return;
        }

        if (SlowDownDroppedTicksDesired > 0)
        {
            if (SlowDownDroppedTicksCurrent >= SlowDownDroppedTicksDesired)
            {
                SlowDownDroppedTicksCurrent = 0;
            }
            else
            {
                SlowDownDroppedTicksCurrent++;
                return; // just ignore Timer tick
            }
        }

        // begin
        if (TickTimerMutex.tryLock() == false)
        {
            // don't accumulate waiting ticks (happened in tests), we don't lose anything if returning here
            TickTimerEventsLostCount++;
            if (TickTimerEventsLostCount > 2000000000)
                TickTimerEventsLostCount = 2000000000; // just to make sure, counter just stops, user will conclude anyway there were MANY ticks lost/delayed ;-P
            IOProgram.SetTickLostCount(TickTimerEventsLostCount);
            return;
        }

        GUIElements.GetMainWindow().DisableRedraw(); // redrawn when enabled again, if some dirty flag set

        ObjectStorage.ResetUpdateHormoneStrengthForAll();

        ObjectStorage.ExciteLines();

        ObjectStorage.TransferActivatedTextFieldExcitement();

        ObjectStorage.AfterCollectingAllExcitement();

        int IOProgramLineExecutedCurrent;

        while (true) // "continue;" if ILine is a comment, else "break;" at end
        {
            IOProgramLineExecutedCurrent = (int) Math.min((long) SimulationIOProgramLineNumberMax, SimulationTickData.TickCount / TicksPerIOTextAreaLine);

            IOProgram.SetIOProgramLineExecutedCurrent(IOProgramLineExecutedCurrent);

            if ((SimulationTickData.TickCount % TicksPerIOTextAreaLine) == 0) // next ILine to be processed?
            {
                String ILine = IOProgram.GetILine(IOProgramLineExecutedCurrent);

                if (ILine == null) // all ILines worked up (and no REPEAT)?
                {
                    break;
                }

                if (ILine.startsWith("// ")) // comment?
                {
                    SimulationTickData.TickCount += (1 * TicksPerIOTextAreaLine);
                    continue; // check next line instantly (in while (true)-loop)
                }
            }
            break; // "continue;" if ILine is a comment
        }

        ProcessActivatedObjects();

        IOProgram.DoRedraw();

        ObjectStorage.LoseExcitementForAllObjects();

        if ((SimulationTickData.TickCount % TicksPerIOTextAreaLine) == 0) // next ILine to be processed?
        {
            String ILine = IOProgram.GetILine(IOProgramLineExecutedCurrent);

            if (ILine == null) // all ILines worked up (and no REPEAT)?
            {
                GUIElements.GetMainWindow().EnableRedraw();
                // StopTickTimer(); // do not stop timer, or MainWindow title gets screwed up and the user could not do manual activation any more
                TickTimerMutex.unlock();
                return;
            }

            switch (ILine)
            {
            case "REPEAT":
                SimulationTickData.TickCount = 0;
                IOProgram.ClearOLines();
                GUIElements.GetMainWindow().EnableRedraw();
                TickTimerMutex.unlock();
                return;
            }

            if (ILine.indexOf(IOProgram.MANUAL_ACTIVATION) == 0)
            {
                SimulationTickData.TickCount = 0;
                // keep OLines, no need to delete them, user might want to view, we don't alter them now anyway
                GUIElements.GetMainWindow().EnableRedraw();
                TickTimerMutex.unlock();
                return;
            }

            if (ILine.indexOf("GOTO=") == 0)
            {
                String GotoTarget = ILine.substring("GOTO=".length());
                for (int i = 0; i < SimulationIOProgramLineNumberMax; i++)
                {
                    if (IOProgram.GetILine(i) == null)
                        break;
                    if (IOProgram.GetILine(i).equals(GotoTarget))
                    {
                        SimulationTickData.TickCount = (i * TicksPerIOTextAreaLine);
                        // keep OLines (currently, to be tried out...)
                        GUIElements.GetMainWindow().EnableRedraw();
                        TickTimerMutex.unlock();
                        return;
                    }
                }
            }

            if (ILine.indexOf("DROPPED_TICKS=") == 0)
            {
                String DroppedTicksString = ILine.substring("DROPPED_TICKS=".length());
                try
                {
                    SlowDownDroppedTicksDesired = Integer.parseInt(DroppedTicksString);
                    if (SlowDownDroppedTicksDesired < 0)
                        SlowDownDroppedTicksDesired = 0;
                    if (SlowDownDroppedTicksDesired > 10)
                        SlowDownDroppedTicksDesired = 10;

                    SlowDownDroppedTicksCurrent = 0;
                }
                catch (Exception e)
                {
                    // ignore wrong number format
                }
            }

            if (ILine.indexOf("=") >= 0)
            {
                ObjectStorage.UpdateHormoneStrengthForAll(Neuron.ExtractHormoneName(ILine), Neuron.ExtractHormoneStrength(ILine));
            }

            String[] ILineParts = ("/" + ILine + "/").split("/");
            for (int p = 0; p < ILineParts.length; p++)
            {
                if (ILineParts[p].length() >= 1)
                {
                    ObjectStorage.ExciteTextFields(ILineParts[p]);
                }
            }
        }

        SimulationTickData.TickCount++; // do at very end, code relies on that

        GUIElements.GetMainWindow().EnableRedraw();

        TickTimerMutex.unlock();
        return;
    }

    public void StartSimulation()
    {
        // NOTE: we set GUIElements' state per method calls to make sure they
        // are in sync with drawn stuff (don't use multi-threading)!

        SimulationTickData = new SimulationTickData();

        Ticker.Clear();

        SlowDownDroppedTicksDesired = 0;
        SlowDownDroppedTicksCurrent = 0;

        TickTimerMutex = new ReentrantLock();
        TickTimerTask = new TimerTask()
        {
            @Override
            public void run()
            {
                (new SimulationTickThread()).start();
            }
        };

        TickTimer = new Timer();
        TickTimerRunning = true;
        TickTimerEventsLostCount = 0;
        TickTimer.schedule(TickTimerTask, 0, (1000 / TicksPerSecond));
    }

    public void StopSimulation()
    {
        StopTickTimer(); // SimulationTick() must react to this
    }

    private void StopTickTimer()
    {
        TickTimerMutex.lock();
        if (TickTimerRunning)
        {
            TickTimer.cancel();
            TickTimer.purge(); // allows creating a new TimerTask, else exception (tested)
            TickTimerRunning = false;
        }
        TickTimerMutex.unlock();
    }
}
